「詳解 AWS Lambdaコールドスタート」というテーマでClassmethod ODYSSEYに登壇しました #cm_odyssey #devio2024
Classmethod ODYSSEY OnlineとDevelopersIO 2024 OSAKAで「詳解 AWS Lambdaコールドスタート」というテーマで登壇させて頂きました。このブログでは登壇内容を抜粋しながら紹介していきます。
内容
コールドスタート時のLambdaのライフサイクル
Lambdaのコールドスタートはざっくり以下のような流れです
コールドスタートの過程で以下4つのフェーズが実行されています
- Create execution environment... Lambda実行環境が構築される
- Download code... 我々ユーザーがデプロイしたコードがLambda実行環境にダウンロードされる
- Start runtime... Node.jsやPythonなど各言語のランタイムが起動する
- Initialize function code... 我々ユーザーがデプロイしたコードのhandler外の部分が実行される
これらのフェーズはAWS側が最適化するフェーズと我々ユーザーが自分自身で最適化するフェーズに分かれます。コールドスタートとうまく付き合うにはAWSが最適化する領域について最適化の恩恵を受けやすい構成にすること、ユーザー側が最適化すべき領域を最適化することが重要です。AWSが最適化する領域についてはユーザー側では何も手出しができないと思うかもしれません。確かに内部処理などの深い部分に手出しはできませんが、AWSがどのような最適化を施しているかを理解すれば最適化の恩恵を受けやすいようにLambdaの設定を工夫することは可能です。
Lambda実行環境の構築について
まずCreate execution environmentの部分について深堀りしていきます
Lambdaの裏側は上記のようなレイヤ構成になっています。まず物理的なハードウェアとベアメタルのEC2インスタンスがあります。このベアメタルのEC2インスタンスは「Woker」や「Lambda Worker」と呼ばれます。
ベアメタルEC2インスタンスの上ではFirecrackerというハイパーバイザが稼働しており、その上で軽量な仮想マシンであるMicroVMとゲストOSのAmazon Linuxが動きます。さらにゲストOSの上にはサンドボックス環境と呼ばれるLambdaのコードを実行するための環境が構築されます。1つの物理マシン上で複数のMicroVMが稼働しますが、1つのMicroVMの上で稼働するサンドボックス環境は1つまでです。1つのMicroVMの上に2つ以上のサンドボックス環境が構築されることはありません。
もう1つ外側のレイヤーに目を向けるとMicroManagerというコンポーネントが存在します。このコンポーネントは起動済みのMicroVMのスロットを管理しており、このMicroManagerが他のコンポーネントと協調することでコールドスタート時に1からMicroVMを起動しなくてもLambdaのコードが実行できるようになっています。
実はコールドスタートにも種類があるということがre:Invent2020のセッションで言及されており、MicroVMの起動を伴うパターンをFull cold start、MicroVMの起動を伴わないパターンをPartial cold startと呼びます。我々ユーザーはログ等の情報からFull cold startとPartial cold startを判断することはできませんが、内部的にはこのようなパターンが存在することを理解しておくと、コールドスタート関連のメトリクスを分析する時に1段深い考察ができるようになります。知っていて損は無い情報だと思います。
LambdaのコードDL処理について
続いてDownload Codeの部分についてです。まずはコンテナイメージ形式のLambdaについて深堀りしていきます。
コンテナイメージ形式のウリとして最大10GBのコンテナイメージがデプロイできるというものがあります。ZIP形式の場合は最大250MBなので、コレは大きな違いです。が、10GBのコンテナイメージと言われるとダウンロード時間が気になるのではないでしょうか?コールドスタート時に10GBのコンテナイメージのダウンロードが実行されるとなると、コールドスタートの所要時間はとても長くなってしまいそうです。しかし、実際には条件さえ整えば10GBのコンテナイメージであっても数百ms程度でコールドスタート可能です。これはLambdaの基盤側で様々な最適化が施されているためです。
コンテナイメージ形式のLambdaはOCI形式のイメージをそのまま利用するのではなく、コンテナイメージを小さなチャンク単位に分割してS3に保存しています。そして、このチャンクは必要なタイミングでオンデマンドでロードされます。例えばコンテナイメージの中に/var/hogehoge
というファイルが存在したとして、コールドスタート時にこのファイルに相当するチャンクはLambda実行環境にロードされていません。Lambdaの処理の中で/var/hogehoge
にアクセスしたタイミングで初めてチャンクがロードされるのです。
さらにコンテナイメージのチャンクは以下3つのキャッシュを活用しています。
- Dedicated local cach... AWSアカウントを跨いで共有されないキャッシュ
- Shard local cache... AWSアカウントを跨いで共有されるキャッシュ Worker上に存在する
- Shared AZ-local cache... AZごとに保持されるキャッシュ
これらのキャッシュを活用することで極力S3へのアクセスが発生しないように工夫されています。
これまでコンテナイメージ形式について説明してきましたが、ZIP形式についてはどうでしょうか?同様に複数のキャッシュを活用するような構成になっているのでしょうか?これについてはre:Inventのセッション等でも特に明確に言及された情報は無い認識です※もしご存知の方いれば教えてください
私個人の妄想としてはZIP形式のパッケージについても都度S3からDLするのではなくWorker上でキャッシュしていると想像しています。以前サーバーレス今昔物語というイベントにて、当時AWSJにお勤めだった西谷さんに質問してみましたが、「回答できません」とのことでした(それはそう)
Init処理について
最後にInit処理についてです。Init処理はhandlerの外側のコードでLambdaがコールドスタートした時だけ実行されます。handlerの外側でグローバル変数を宣言し、DBとの接続を管理するオブジェクト等をグローバル変数にセットすることがLambdaのベストプラクティスとして知られています。このグローバル変数の話とセットでよく出てくるのが遅延実行の話です。遅延実行はざっくりいうと以下のようなイメージです。
boto3のクライアントを生成する処理がInvokeフェーズに移動したことで確かにInitフェーズは高速化しますが、その分初回のInvokeフェーズは遅くなります。ユーザーから見たLambdaの処理時間はInitフェーズ + Invokeフェーズなので、単純な遅延実行は特にコールドスタートの改善には繋がらないと捉えることができます。
このようなコードであればコールドスタート時は(条件次第で)boto3のクライアント生成がスキップされ、次回ウォームスタート時にboto3のクライアントが生成されることになるので、重い処理が全体に分散されて全体最適化が図られるという考え方もできます。しかし、このようなケースにおいても遅延実行にデメリットがあるという話を深堀りして考えていきます。
これは先ほど例示した遅延実行なし/ありのコードをそれぞれ実行して比較した結果です。遅延実行なしの方が合計の処理時間が早くなっており、先程の説明と矛盾しています。なぜこのようなことが起こるのでしょうか?ヒントはメモリ割り当て128Mで計測したという点です。
この現象の裏にはboost host CPUという仕様が関係しています。通常Lambda実行環境のCPUパワーはメモリの割当に比例します。メモリを1769MB割り当てた時点で1vCPU分のフルパワーが使えるようになるのですが、実はコレ、Invokeフェーズだけの話なんです。Initフェーズについてはメモリ128MBであってもブーストしたCPUパワーで処理が行えるようになっています。これを踏まえて先ほどのコード例を再確認してみましょう。
遅延実行なしの場合はメモリ128MBであってもブーストしたCPUパワーでboto3のクライアントを生成できるため高速に処理が完了します。
それに対して遅延実行ありの場合はメモリ割り当てに応じた貧弱なCPUパワーでboto3のクライアントを生成することになります。boto3のクライアント生成処理はJSONファイルを読み込んでクラスを動的に定義する処理が動くため、CPUパワーが貧弱だとその分処理時間も長くなってしまうのです。
※2回目以後のクライアント生成処理はキャッシュを利用するので高速になります
ということで、Lambdaのメモリ割り当て追加はInitフェーズの高速化にはつながりません。高速化されるのはあくまでhandlerフェーズだけです。誤解しやすいので覚えておきましょう。
言語別のTIPS(.NETの場合)
.NET(C#)の場合はNatice AOT形式を利用することが有効と公式ドキュメントでも紹介されています。.NET8以前のランタイムを利用している場合はバージョンアップを検討してみてはいかがでしょうか?
言語別のTIPS(Node.jsの場合)
続いてNode.jsについてです。Node.jsに限った話ではないですが、まずランタイムのバージョンを上げるというのが有効な選択肢になります。
Node.js20xからはAmazonのRoot CA証明書読み込み周りの挙動が変わっており条件次第では30ms程度の改善につながることもあります。
言語別のTIPS(Javaの場合)
Javaの場合はSnapStartが利用できるので、これを使うのが有効な選択肢となります。SnapStartの裏側ではFirecrackerのスナップショット技術が利用されているため、Lambdaのメモリ割り当てが増えるとスナップショットのサイズが肥大化し、コールドスタートが遅くならないか心配になるかもしれません。
実際にはSnapStartの裏側ではコンテナイメージ形式のLambdaと類似の最適化が図られているため、メモリ割当に比例してコールドスタートが遅くなるということはありません。安心してSnapStartを利用しましょう。
言語別のTIPS(Pythonの場合)
Pythonに関しては黒魔術的なテクニックを紹介します。正直利用はオススメしないので、これらのテクニックは使わないことをオススメします。
Python向けのAWS CDKであるboto3,botocoreにはAWSの各種サービスのAPI仕様を定義したJSONファイルが含まれています。通常のユースケースであればLambdaからAWSのAPIを呼び出す際に全てのAPIの仕様が必要になることはないでしょう。利用しないサービスのAPIを定義したJSONファイルは事前に削除してからデプロイパッケージを作ることで、コードのダウンロード時間の短縮が図れます。
さらに、このJSONファイルはGZIP圧縮されていることがあります。圧縮されていればそれだけコードのダウンロード時間が短くなりますが、同時にboto3のクライアントを生成する際にGZIPを解凍する処理が必要になります。事前にGZIPを解凍したJSONファイルをパッケージングすればLambda実行環境でGZIPを解凍するオーバーヘッドが削減できるため、微妙に処理の高速化が図れます。
この計測結果は恣意的に数字を作りにいったところがあって、CPUパワーが貧弱になるようにメモリ割り当てを128MにしたうえでInvokeフェーズでboto3のクライアントを生成しているので、実際にはここまで大きな違いが出ることは無いでしょう。Initフェーズでクライアントを初期化すればGZIP解凍有無による違いは誤差レベルしか発生しません。
登壇資料
全体はSpeaker Deckにアップしています
参考資料
Youtube動画
- AWS Lambda Under the Hood
- AWS re:Invent 2020: AWS Lambda – Part 2: Optimizing your Lambda function performance
- AWS re:Invent 2020: Deep dive into AWS Lambda security: Function isolation
- AWS re:Invent 2019: [REPEAT 1] Best practices for AWS Lambda and Java (SVS403-R1)
- サーバーレスアンチパターン今昔物語 第五夜 - 解体新書 -
- Ahead of time: Optimize your Java application on AWS Lambda
論文・セッションスライド等
- AWS Lambda Under the Hood
- Security Overview of AWS Lambda
- Firecracker: Lightweight Virtualization for Serverless Applications
- Deep dive into AWS Lambda security: Function isolation
- AWS re:Invent 2019: [REPEAT 1] Best practices for AWS Lambda and Java (SVS403-R1)
- AWS re:Invent 2020: Ahead of time: Optimize your Java application on AWS Lambda
ブログ・ドキュメント類
- コンテナイメージとしてパッケージ化された Lambda 関数の最適化 | Amazon Web Services ブログ
- Comparing the effect of global scope - AWS Lambda
- .zip または JAR ファイルアーカイブで Java Lambda 関数をデプロイする - AWS Lambda
- AWS Lambda 関数を使用するためのベストプラクティス - AWS Lambda
- Lambdaのメモリ割り当てが増えてもSnapStartのRestore Durationが変わらないことを確認してみた | DevelopersIO
- LambdaのINITフェーズではメモリ128MでもCPUパワーをフルに使える?!boost host CPUの動きを確認してみた | DevelopersIO
- 環境変数NODE_EXTRA_CA_CERTSがコールドスタートに与える影響を確認してみた | DevelopersIO
- コールドスタート高速化のためにboto3をスリム化して改善効果を測定してみた | DevelopersIO
- あなたのLambdaが動いているのはEC2の上?それともFirecrackerの上? | DevelopersIO